package com.glsx.auto;

import com.github.javaparser.JavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.expr.*;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.ReturnStmt;
import com.github.javaparser.ast.type.ClassOrInterfaceType;
import com.google.common.base.CaseFormat;
import com.google.common.base.Charsets;
import com.google.common.collect.Lists;
import com.google.common.io.Files;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.sql.SQLException;
import java.util.List;

import static com.github.javaparser.ast.expr.AssignExpr.Operator.ASSIGN;
import static com.glsx.auto.Constant.*;
import static com.glsx.auto.DefaultPackage.*;
import static com.glsx.auto.Utils.javaFile;


// http://stackoverflow.com/questions/37208935/javaparser-annotationexpr-get-parameters

public abstract class AbstractAutoMojo  extends AbstractMojo {

    private static final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

    public ProjectModule common;
    public ProjectModule container;

    @Parameter(defaultValue = "${project}")
    public MavenProject mavenProject;

    /**
     * 数据库url
     */
    @Parameter(name="databaseUrl")
    public String databaseUrl;

    /**
     * 数据库用户名
     */
    @Parameter(name="userName")
    public String userName;

    /**
     * 数据库密码
     */
    @Parameter(name="password")
    public String password;

    /**
     * 表格数组
     */
    @Parameter(name="tableNames")
    public String[] tableNames;

    /**
     * 删除表前缀
     */
    @Parameter(name="eraseTablePrefix")
    public String eraseTablePrefix;

    /**
     * 包前缀
     */
    @Parameter(name="topPackageName", defaultValue = DEFAULT_TOP_PACKAGE_PREFIX)
    public String topPackageName;

    @Parameter(name = "servicedir")
    public String servicedir;


    @Parameter(name = "hibernateVersion", defaultValue = "4.1.7.Final")
    public String hibernateVersion;

    @Parameter(name = "springframeworkVersion", defaultValue = "3.0.0.RELEASE")
    public String springframeworkVersion;

    public boolean createCommonDirectories() {
        return common.createFrameWorkDirectories(false);
    }
    public boolean createContainerDirectories() {
        return container.createFrameWorkDirectories(true);
    }

    private static void writeCompilationUnit(CompilationUnit cu, File file) {
        try {
            Files.write(cu.toString(), file, Charsets.UTF_8);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    public void createHibernateMapper(CompilationUnit cu, ClassOrInterfaceDeclaration cd, Table table) {

        String clzName = cd.getName().getIdentifier();
        String mappingFilePath = container.getResourceEntityHbmMapperDirectroy() + File.pathSeparatorChar + clzName + ".hbm.xml";

        String qualifiedTypeName = cd.getNameAsString();
        if(cu.getPackageDeclaration().isPresent()) {
            qualifiedTypeName = cu.getPackageDeclaration().get().getNameAsString() + "." + qualifiedTypeName;
        }

        try {
            Document document = null;
            Element rootEle = null;
            Element aClass = null;
            File file = new File(mappingFilePath);
            if(file.exists()) {
                document = dbf.newDocumentBuilder().parse(file);
                rootEle = document.getDocumentElement();
                aClass = document.getElementById("class");
            } else {
                document = dbf.newDocumentBuilder().newDocument();
                rootEle = document.createElement("hibernate-mapping");
                aClass = document.createElement("class");
                aClass.setAttribute("name", qualifiedTypeName);
                aClass.setAttribute("table", table.getTableName());
            }

            for (Table.Column column : table.getColumns()) {
                Element property = document.createElement(column.isPrimaryKey() ? "id" : "property");

                property.setAttribute("name", column.getFieldName());
                property.setAttribute("type", column.getType().getName());

                Element col = document.createElement("column");
                col.setAttribute("name", column.getName());
                property.appendChild(col);

                if(!column.getComment().isEmpty()) {
                    Element comment = document.createElement("comment");
                    comment.setTextContent(column.getComment());
                    col.appendChild(comment);
                }

                if(column.isPrimaryKey()) {
                    Element generator = document.createElement("generator");
                    generator.setAttribute("class", "identity");
                    property.appendChild(generator);
                }

                aClass.appendChild(property);
            }

            if(!file.exists()) {
                rootEle.appendChild(aClass);
                document.appendChild(rootEle);
            }

            Transformer tr = TransformerFactory.newInstance().newTransformer();
            tr.setOutputProperty(OutputKeys.METHOD, "xml");
            tr.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
            tr.setOutputProperty(OutputKeys.VERSION, "1.0");
            tr.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd");
            tr.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "-//Hibernate/Hibernate Mapping DTD 3.0//EN");

            tr.transform(new DOMSource(document),
                    new StreamResult(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")));

        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            e.printStackTrace();
        } catch (TransformerConfigurationException e) {
            e.printStackTrace();
        } catch (TransformerException e) {
            e.printStackTrace();
        }
    }

    private CompilationUnit parseSrcTemplate(String srcTemplate, String packageDeclaration, List<String> imports) {
        CompilationUnit cu = JavaParser.parse(Utils.getResourceAsStream(srcTemplate));
        if(!StringUtils.isEmpty(packageDeclaration)) {
            cu.setPackageDeclaration(packageDeclaration);
        }
        if(imports != null) {
            for (String name : imports) {
                cu.addImport(name);
            }
        }
        return cu;
    }

    private void addIService() {
        File file = javaFile(common.getPackageDirectory(COMMON_BASE), I_SERVICE);
        if(!file.exists()) {
            writeCompilationUnit(parseSrcTemplate(ISERVICE_TEMPLATE,
                    common.getPackageName(COMMON_BASE), null), file);
        }
    }

    private void addIServiceImpl() {
        File file = javaFile(container.getPackageDirectory(CONTAINER_SERVICE_IMPL), I_SERVICE_IMPL);
        if(!file.exists()) {
            writeCompilationUnit(parseSrcTemplate(SERVICE_IMPL_TEMPLATE,
                    container.getPackageName(CONTAINER_SERVICE_IMPL),
                    Lists.newArrayList(
                            container.getPackageName(CONTAINER_DAO)+"."+BASE_DAO,
                            common.getPackageName(COMMON_BASE)+".*")), file);
        }
    }

    private void addServiceException() {
        File file = javaFile(common.getPackageDirectory(COMMON_BASE), SERVICE_EXCEPTION);
        if(!file.exists()) {
            writeCompilationUnit(parseSrcTemplate(SERVICE_EXCEPTION_TEMPLATE,
                    common.getPackageName(COMMON_BASE), null), file);
        }
    }

    public void addBaseDao() {
        File file = javaFile(container.getPackageDirectory(CONTAINER_DAO), BASE_DAO);
        if(!file.exists()) {
            writeCompilationUnit(parseSrcTemplate(BASE_DAO_SRC_TEMPLATE,
                    container.getPackageName(CONTAINER_DAO), Lists.newArrayList(
                            HIBERNATE_SESSION_FACTORY_PACKAGE,
                            HIBERNATE_SESSION_PACKAGE,
                            "org.springframework.beans.factory.annotation.Autowired"
                            )), file);
        }
    }

    public void addEntity(Table table) {

        String javaBeanName = table.getJavaBeanName();
        File file = javaFile(common.getPackageDirectory(COMMON_ENTITY), javaBeanName);
        if(!file.exists()) {

            CompilationUnit cu = new CompilationUnit(common.getPackageName(COMMON_ENTITY));
            ClassOrInterfaceDeclaration cd = cu.addClass(javaBeanName);
            if(!StringUtils.isEmpty(table.getComment())) {
                cd.setJavadocComment(table.getComment());
            }
            cd.addImplementedType(Serializable.class);

            for (Table.Column column : table.getColumns()) {
                cd.addPrivateField(column.getType(), column.getFieldName())
                        .setJavadocComment(column.getComment());
            }

            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("\"").append(javaBeanName).append("{\"").append(" + ").append(System.lineSeparator());

            for (Table.Column column : table.getColumns()) {
                stringBuilder.append("\t\t\t\t\"").append(column.getFieldName()).append("'=\"")
                        .append(" + ")
                        .append(column.getFieldName())
                        .append(" + ")
                        .append("'\\\''")
                        .append(" + ")
                        .append(System.lineSeparator());

                cd.addMethod(column.toGetMethodName(), Modifier.PUBLIC)
                        .setType(column.getType())
                        .setBody(new BlockStmt().addStatement(new ReturnStmt(column.getFieldName())));
                cd.addMethod(column.toSetMethodName(), Modifier.PUBLIC)
                        .addParameter(new com.github.javaparser.ast.body.Parameter(new ClassOrInterfaceType(column.getType().getSimpleName()), column.getFieldName()))
                        .setBody(new BlockStmt().addStatement(new AssignExpr(new NameExpr("this." + column.getFieldName()), new NameExpr(column.getFieldName()), ASSIGN)));
            }

            stringBuilder.append("\t\t\t\t'}'");
            cd.addMethod("toString", Modifier.PUBLIC)
                    .addAnnotation(new MarkerAnnotationExpr("Override"))
                    .setType(String.class)
                    .setBody(new BlockStmt().addStatement(new ReturnStmt(stringBuilder.toString())));

            writeCompilationUnit(cu, file);
            createHibernateMapper(cu, cd, table);
        }
    }

    public void addEntityDao(Table table) {
        String javaBeanName = table.getJavaBeanName();
        String daoName = javaBeanName+"Dao";
        File file = javaFile(container.getPackageDirectory(CONTAINER_DAO), daoName);
        if(!file.exists()) {
            CompilationUnit cu = new CompilationUnit(container.getPackageName(CONTAINER_DAO));
            cu.addImport(common.getPackageName(COMMON_ENTITY)+"."+ table.getJavaBeanName());
            cu.addImport("org.springframework.stereotype.Repository");
            ClassOrInterfaceDeclaration cd = cu.addClass(daoName);
            cd.addAnnotation(new MarkerAnnotationExpr("Repository"));
            cd.addExtendedType(BASE_DAO + "<"+javaBeanName+","+ table.getPrimaryColumn().getType().getSimpleName()+">");
            writeCompilationUnit(cu, file);
        }
    }

    public void addEntityService(Table table) {
        String javaBeanName = table.getJavaBeanName();
        String serviceName = javaBeanName+"Service";
        File file = javaFile(common.getPackageDirectory(COMMON_SERVICE), serviceName);
        if(!file.exists()) {
            CompilationUnit cu = new CompilationUnit(common.getPackageName(COMMON_SERVICE));
            cu.addImport(common.getPackageName(COMMON_BASE)+".*");
            cu.addImport(common.getPackageName(COMMON_ENTITY)+"."+javaBeanName);
            ClassOrInterfaceDeclaration cd = cu.addInterface(serviceName);
            if(!StringUtils.isEmpty(table.getComment())) {
                cd.setJavadocComment(table.getComment());
            }
            cd.addExtendedType(I_SERVICE+"<"+javaBeanName+","+ table.getPrimaryColumn().getType().getSimpleName()+">");
            writeCompilationUnit(cu, file);
        }
    }

    public void addEntityServiceImpl(Table table) {
        String javaBeanName = table.getJavaBeanName();
        String serviceName = javaBeanName+"Service";
        String daoName = javaBeanName+"Dao";
        String serviceImplName = javaBeanName+"ServiceImpl";
        File file = javaFile(container.getPackageDirectory(CONTAINER_SERVICE_IMPL), serviceImplName);
        if(!file.exists()) {
            CompilationUnit cu = new CompilationUnit(container.getPackageName(CONTAINER_SERVICE_IMPL));
            cu.addImport(common.getPackageName(COMMON_BASE)+".*");
            cu.addImport(common.getPackageName(COMMON_ENTITY)+"."+javaBeanName);
            cu.addImport(common.getPackageName(COMMON_SERVICE)+"."+serviceName);
            cu.addImport(container.getPackageName(CONTAINER_DAO)+"."+daoName);
            cu.addImport("com.alibaba.dubbo.config.annotation.Service");
            cu.addImport("org.springframework.beans.factory.annotation.Autowired");
            ClassOrInterfaceDeclaration cd = cu.addClass(serviceImplName);
            cd.addAndGetAnnotation("Service").addPair("version", "\"1.0.0\"");
            cd.addImplementedType(serviceName);
            cd.addExtendedType(I_SERVICE_IMPL+"<"+javaBeanName+","+ table.getPrimaryColumn().getType().getSimpleName()+","+daoName+">");

            String entityDaoFieldName = Utils.typeToName(daoName);
            ConstructorDeclaration constructorDeclaration = cd.addConstructor(Modifier.PUBLIC);
            constructorDeclaration.addAnnotation(new MarkerAnnotationExpr("Autowired"));

            constructorDeclaration.addParameter(daoName, entityDaoFieldName);
            BlockStmt blockStmt = new BlockStmt();
            NodeList<Expression> arguments = new NodeList<Expression>();
            arguments.add(new NameExpr(entityDaoFieldName));
            blockStmt.addStatement(new MethodCallExpr(null, new SimpleName("super"), arguments));
            constructorDeclaration.setBody(blockStmt);
            writeCompilationUnit(cu, file);
        }
    }

//    private void addDubboConfig(String serviceName) {
//        try {
//
//            File file = new File(container.getResourceConfigDirectory()+"/platform-dubbo-provider.xml");
//
//            if(!file.exists()) {
//                return;
//            }
//
//            Document document = dbf.newDocumentBuilder().parse(file);
//            Element beans = document.getDocumentElement();
//
//            Element service = document.createElement("dubbo:service");
//            service.setAttribute("ref", typeToName(serviceName));
//            service.setAttribute("interface", common.getPackageName(COMMON_SERVICE)+"."+serviceName+"Impl");
//            service.setAttribute("version", "1.0.0");
//            service.setAttribute("timeout", "100000");
//            service.setAttribute("retries", "0");
//            service.setAttribute("protocol", "dubbo");
//
////            Element bean = document.createElement("bean");
////            bean.setAttribute("id", typeToName(serviceName));
////            bean.setAttribute("class", container.getPackageName(CONTAINER_SERVICE_IMPL)+"."+serviceName+"Impl");
//
//            beans.appendChild(service);
////            beans.appendChild(bean);
//
//            Transformer tr = TransformerFactory.newInstance().newTransformer();
//
//            tr.transform(new DOMSource(document),
//                    new StreamResult(new OutputStreamWriter(new FileOutputStream(file), "UTF-8")));
//
//
//        } catch (SAXException e) {
//            e.printStackTrace();
//        } catch (IOException e) {
//            e.printStackTrace();
//        } catch (ParserConfigurationException e) {
//            e.printStackTrace();
//        } catch (TransformerConfigurationException e) {
//            e.printStackTrace();
//        } catch (TransformerException e) {
//            e.printStackTrace();
//        }
//    }

    private CompilationUnit parse(File file) {
        if(file != null && file.exists()) {
            try {
                return JavaParser.parse(file);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    private DocVoidVisitorAdapter visitor(CompilationUnit cu) {
        return new DocVoidVisitorAdapter(cu);
    }

    public void updateOrCreateSrc() {
        try  {

            addBaseDao();
            addIService();
            addIServiceImpl();
            addServiceException();

            TableFactory tableFactory = new TableFactory(databaseUrl, userName, password);

            for (String tableName : tableNames) {

                Table table = tableFactory.fetchTable(tableName, eraseTablePrefix);

                if(table.getPrimaryColumn() == null) {
                    getLog().warn(tableName + " primary key not present");
                    continue;
                }

                addEntity(table);
                addEntityDao(table);
                addEntityService(table);
                addEntityServiceImpl(table);
            }
        }catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        String basename = CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, mavenProject.getName());
        common = new ProjectModule(mavenProject.getBasedir().getAbsolutePath(), basename+"-common", topPackageName);
        container = new ProjectModule(mavenProject.getBasedir().getAbsolutePath(), basename+"-container", topPackageName);
        process();
    }

    public abstract void process();
}
